<?php declare(strict_types=1);

namespace Shopware\Core\System\SystemConfig\Util;

use Shopware\Core\Framework\Bundle;
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\Framework\Util\XmlReader;
use Shopware\Core\System\SystemConfig\Exception\BundleConfigNotFoundException;
use Shopware\Core\System\SystemConfig\SystemConfigException;

#[Package('framework')]
class ConfigReader extends XmlReader
{
    public const INPUT_TYPE_BOOL = 'bool';
    public const INPUT_TYPE_CHECKBOX = 'checkbox';
    public const INPUT_TYPE_INT = 'int';
    public const INPUT_TYPE_FLOAT = 'float';
    public const INPUT_TYPE_MULTI_SELECT = 'multi-select';

    private const FALLBACK_LOCALE = 'en-GB';

    protected string $xsdFile = __DIR__ . '/../Schema/config.xsd';

    /**
     * @throws BundleConfigNotFoundException
     *
     * @return array<array<string, mixed>>
     */
    public function getConfigFromBundle(Bundle $bundle, ?string $bundleConfigName = null): array
    {
        if ($bundleConfigName === null) {
            $bundleConfigName = 'Resources/config/config.xml';
        } else {
            $bundleConfigName = 'Resources/config/' . preg_replace('/\\.xml$/i', '', $bundleConfigName) . '.xml';
        }
        $configPath = $bundle->getPath() . '/' . ltrim($bundleConfigName, '/');

        if (!is_file($configPath)) {
            throw SystemConfigException::bundleConfigNotFound($bundleConfigName, $bundle->getName());
        }

        return $this->read($configPath);
    }

    /**
     * This method is the main entry point to parse a xml file.
     */
    protected function parseFile(\DOMDocument $xml): array
    {
        return $this->getCardDefinitions($xml);
    }

    /**
     * @return array<array{title: array<string, string|null>, name: string|null, elements: list<array<string, mixed>>, flag?: string|null}>
     */
    private function getCardDefinitions(\DOMDocument $xml): array
    {
        $cardDefinitions = [];

        foreach ($xml->getElementsByTagName('card') as $element) {
            $cardDefinition = [
                'title' => $this->getCardTitles($element),
                'name' => $this->getCardName($element),
                'elements' => $this->getElements($element),
            ];

            if ($this->getCardFlag($element) !== null) {
                $cardDefinition['flag'] = $this->getCardFlag($element);
            }

            $cardDefinitions[] = $cardDefinition;
        }

        return $cardDefinitions;
    }

    /**
     * @return array<string, string|null>
     */
    private function getCardTitles(\DOMElement $element): array
    {
        $titles = [];
        foreach ($element->getElementsByTagName('title') as $title) {
            $titles[$this->getLocaleCodeFromElement($title)] = $title->nodeValue;
        }

        return $titles;
    }

    /**
     * @return list<array<string, mixed>>
     */
    private function getElements(\DOMElement $xml): array
    {
        $elements = [];
        foreach (static::getAllChildren($xml) as $element) {
            $nodeName = $element->nodeName;
            if (\in_array($nodeName, ['title', 'name', 'flag'], true)) {
                continue;
            }

            $elements[] = $this->elementToArray($element);
        }

        return $elements;
    }

    private function getCardName(\DOMElement $element): ?string
    {
        foreach ($element->getElementsByTagName('name') as $name) {
            $parentNode = $name->parentNode;
            if (($parentNode !== null) && $parentNode->nodeName !== 'card') {
                continue;
            }

            return $name->nodeValue;
        }

        return null;
    }

    private function getCardFlag(\DOMElement $element): ?string
    {
        foreach ($element->getElementsByTagName('flag') as $flag) {
            $parentNode = $flag->parentNode;
            if (($parentNode !== null) && $parentNode->nodeName !== 'card') {
                continue;
            }

            return $flag->nodeValue;
        }

        return null;
    }

    /**
     * @return array<string, mixed>
     */
    private function elementToArray(\DOMElement $element): array
    {
        $options = static::getAllChildren($element);

        if ($element->nodeName === 'component') {
            return $this->getElementDataForComponent($element, $options);
        }

        return $this->getElementDataForInputField($element, $options);
    }

    /**
     * @param list<\DOMElement> $options
     *
     * @return array<string, mixed>
     */
    private function getElementDataForComponent(\DOMElement $element, array $options): array
    {
        $elementData = [
            'componentName' => $element->getAttribute('name'),
        ];

        return $this->addOptionsToElementData($options, $elementData);
    }

    /**
     * @param list<\DOMElement> $options
     *
     * @return array<string, mixed>
     */
    private function getElementDataForInputField(\DOMElement $element, array $options): array
    {
        $swFieldType = $element->getAttribute('type') ?: 'text';

        $elementData = [
            'type' => $swFieldType,
        ];

        return $this->addOptionsToElementData($options, $elementData);
    }

    /**
     * @param list<\DOMElement> $options
     * @param array<string, mixed> $elementData
     *
     * @return array<string, mixed>
     */
    private function addOptionsToElementData(array $options, array $elementData): array
    {
        foreach ($options as $option) {
            if ($this->isTranslateAbleOption($option)) {
                $elementData[$option->nodeName][$this->getLocaleCodeFromElement($option)] = $option->nodeValue;

                continue;
            }

            if ($this->isBoolOption($option)) {
                $elementData[$option->nodeName] = filter_var($option->nodeValue, \FILTER_VALIDATE_BOOLEAN);

                continue;
            }

            if ($this->elementIsOptions($option)) {
                $elementData['options'] = $this->optionsToArray($option);

                continue;
            }

            if ($option->nodeName === 'defaultValue') {
                $elementData[$option->nodeName] = $this->parseDefaultValue($option->nodeValue, $elementData['type'] ?? null);

                continue;
            }

            $elementData[$option->nodeName] = $option->nodeValue;
        }

        return $elementData;
    }

    private function parseDefaultValue(?string $value, ?string $type): mixed
    {
        $value = XmlReader::phpize($value);

        if ($value === null) {
            return null;
        }

        return match ($type) {
            // custom elements can have all types, there we can't guarantee the type
            null => $value,
            self::INPUT_TYPE_BOOL, self::INPUT_TYPE_CHECKBOX => (bool) $value,
            self::INPUT_TYPE_INT => (int) $value,
            self::INPUT_TYPE_FLOAT => (float) $value,
            self::INPUT_TYPE_MULTI_SELECT => (array) $value,
            default => (string) $value,
        };
    }

    /**
     * @return array<array{id: string|null, name: array<string, string|null>}>
     */
    private function optionsToArray(\DOMElement $element): array
    {
        $options = [];

        foreach ($element->getElementsByTagName('option') as $option) {
            $idTag = $option->getElementsByTagName('id')->item(0);
            if ($idTag === null) {
                continue;
            }

            $options[] = [
                'id' => $idTag->nodeValue,
                'name' => $this->getOptionLabels($option),
            ];
        }

        return $options;
    }

    /**
     * @return array<string, string|null>
     */
    private function getOptionLabels(\DOMElement $option): array
    {
        $optionLabels = [];

        foreach ($option->getElementsByTagName('name') as $label) {
            $optionLabels[$this->getLocaleCodeFromElement($label)] = $label->nodeValue;
        }

        return $optionLabels;
    }

    private function getLocaleCodeFromElement(\DOMElement $element): string
    {
        return $element->getAttribute('lang') ?: self::FALLBACK_LOCALE;
    }

    private function isTranslateAbleOption(\DOMElement $option): bool
    {
        return \in_array($option->nodeName, ['label', 'placeholder', 'helpText'], true);
    }

    private function isBoolOption(\DOMElement $option): bool
    {
        return \in_array($option->nodeName, ['copyable', 'disabled', 'required'], true);
    }

    private function elementIsOptions(\DOMElement $option): bool
    {
        return $option->nodeName === 'options';
    }
}
